from pygame.sprite import Sprite, RenderUpdates, spritecollide
import pygame
from EventManagement import EventUser
from TackGameEvents import ImageRequestEvent, BackgroundKeyRequestEvent, ModuleLoadEvent
from CoreEvents import LogEvent, TickEvent
from copy import deepcopy

################################################################################
class ViewManager(EventUser):
    """
    Draws screen output, all sprites need to be registered to be drawn
    """
    ############################################################################
    def __init__(self, lScreenResolution, bFullScreen):
        """
        param : lScreenResolution - list containing [x,y]
        param : bFullScreen - True to make screen fullscreen
        """
        # Add event callbacks
        self.RegisterEvent(TickEvent, self.UpdateScreen)
        self.RegisterEvent(ModuleLoadEvent, self.Initialise)
        
        # Initialise class variables
        self._rScreen = None
        self._rBlackBackground = None
        self._bFullScreen = bFullScreen
        self._lScreenResolution = lScreenResolution
        self._rSpriteGroup = RenderDirty()
        self._sCurrentBackgroundKey = None
        self._rCurrentBackgroundImage = None
        self._sNewBackgroundKey = None
        self._rNewBackgroundImage = None
    ############################################################################
    
    ############################################################################
    # Callbacks
    ############################################################################
    ############################################################################
    def Initialise(self, oEvent):
        """
        Initialises pygame bits and draws a black screen
        """
        if self._bFullScreen:
            self._rScreen = pygame.display.set_mode(self._lScreenResolution, pygame.FULLSCREEN)
        else:
            self._rScreen = pygame.display.set_mode(self._lScreenResolution)
        
        # Create black surface
        self._rBlackBackground = pygame.Surface(self._lScreenResolution)
        self._rBlackBackground.fill([0,0,0])
        
        # Draw initial screen black
        self._rScreen.blit(self._rBlackBackground, [0, 0])
        self._rCurrentBackgroundImage = self._rBlackBackground
    ############################################################################
    
    ############################################################################
    def UpdateScreen(self, oEvent):
        """
        Updates screen background and redraws sprites
        """
        # Update background image
        rNewBackgroundRect = self._DrawBackground()
        
        # Update sprites
        self._rSpriteGroup.clear(self._rScreen, self._rCurrentBackgroundImage)
        self._rSpriteGroup.update()
        lDirtyRects = self._rSpriteGroup.draw(self._rScreen)
        
        # Update screen (lDirtyRects set to None if we want whole screen
        # redrawn)
        if rNewBackgroundRect:
            pygame.display.update(rNewBackgroundRect)
        else:
            if len(lDirtyRects) > 0: pygame.display.update(lDirtyRects)
    ############################################################################
    
    ############################################################################
    # Protected Methods
    ############################################################################
    ############################################################################
    def _DrawBackground(self):
        """
        Draws the background image (if required)
        
        return - bNewBackground : True if a new background image has been
            drawn (will require whole screen to be redrawn)
        """
        self._CheckBackgroundKey(self.RequestData(BackgroundKeyRequestEvent()))
        if self._sCurrentBackgroundKey <> self._sNewBackgroundKey:
            # If there is a new backgroundkey, the new background image
            # is drawn to the screen
            self._sCurrentBackgroundKey = self._sNewBackgroundKey
            self._rCurrentBackgroundImage = self._rNewBackgroundImage
            x = (self._rScreen.get_width() - self._rCurrentBackgroundImage.get_width()) / 2
            y = (self._rScreen.get_height() - self._rCurrentBackgroundImage.get_height()) / 2
            return self._rScreen.blit(self._rCurrentBackgroundImage, [x, y])
        return None
    ############################################################################
    
    ############################################################################
    def _CheckBackgroundKey(self, sKey):
        """
        Check recieved background key and update the Background image if
            necessary
        
        param - sKey : Key recieved back from BackgroundKeyRequest
        """
        if not sKey:
            # No background image, will fill background black
            self.SendEvent(LogEvent({
                'sMessage' : "No background key, using blank",
                'nLogLevel' : 1,
            }))
            self._rNewBackgroundImage = self._rBlackBackground
            self._sNewBackgroundKey = None
        elif sKey <> self._sCurrentBackgroundKey:
            # The key has changed, request the new background image
            self.SendEvent(LogEvent({
                'sMessage' : "New background key: " + sKey,
                'nLogLevel' : 0,
            }))
            oNewImage = self.RequestData(ImageRequestEvent({ 'sKey' : sKey }))
            if oNewImage:
                self._rNewBackgroundImage = oNewImage.GetImage()
            else:
                self.SendEvent(LogEvent({
                    'sMessage' : "Couldn't find background image for: " + sKey,
                    'nLogLevel' : 2,
                }))
                self._rNewBackgroundImage = self._rBlackBackground
            self._sNewBackgroundKey = sKey
    ############################################################################
    
    ############################################################################
    def _NewSprite(self, rClient):
        """
        Create a new sprite for a client object
        
        param - rClient : Client object for new sprite
        """
        self._rSpriteGroup.add(DirtySprite(rClient))
    ############################################################################
################################################################################

################################################################################
class DirtySprite(Sprite, EventUser):
    ############################################################################
    def __init__(self, rClient):
        Sprite.__init__(self)
        self._wrClient = rClient
        self.image = None
        self.rect = deepcopy(self.GetClient().GetRect())
        self._sCurrentImageKey = None
        self._bDirty = 0
        self._nExpectedImageEventId = None
        self._sImageState = None
    ################################################################################
    
    ################################################################################
    def update(self):
        """
        OVERRIDE - Implements the update of a Dirty Sprite, checks to see if
            the image has changed or if nay overlaps have changed then it is
            marked as 'Dirty' (e.g needs to be redrawn)
        """
        self._bDirty = 0
        
        # Get image from client
        sNewImageKey = self.GetClient().GetCurrentImageKey()
        if sNewImageKey <> self._sCurrentImageKey:
            # Image has changed, request new image
            self._LoadImage(sNewImageKey)
            
            # If image request was successfull, mark sprite dirty
            if self._sCurrentImageKey == sNewImageKey: self._bDirty = 1
        
        # Check the image state (animatedness!!!)
        sNewImageState = self._rImage.GetState()
        if self._sImageState <> sNewImageState:
            self._sImageState = sNewImageState
            self._bDirty = 1
            if self._rImage: self.image = self._rImage.GetImage()
        
        # Get position from client
        lPosition = self.GetClient().GetCurrentPosition()
        
        if self.rect.topleft != lPosition:
            # Position has changed, update!
            self.rect.topleft = lPosition
            self._bDirty = 1
    ################################################################################
    
    ################################################################################
    def _LoadImage(self, sImageKey):
        """
        Loads an image identified from a given key and sets internal variables
            accordingly
        
        param - sImageKey : Key that identifies new image
        """
        oImageData = self.RequestData(ImageRequestEvent({ 'sKey' : sImageKey }))
        if oImageData:
            # We have received an image!
            self._rImage = oImageData
            self.image = self._rImage.GetImage()
            self._sCurrentImageKey = sImageKey
            self.rect.width = oImageData.get_rect().width
            self.rect.height = oImageData.get_rect().height
            self.GetClient().SetRect(deepcopy(self.rect))
            
            # Set initial Image state
            self._sImageState = self._rImage.GetState()
        else:
            # Error!
            self.SendEvent(LogEvent("Image with key: " + sImageKey + " not found" , 2))
            self._sNewImageKey = None
    ################################################################################
    
    ################################################################################
    def IsDirty(self):
        return self._bDirty
    ################################################################################
    
    ################################################################################
    def SetDirty(self, bDirty):
        self._bDirty = bDirty
    ################################################################################
    
    ################################################################################
    def GetClient(self):
        return self._wrClient()
    ################################################################################
####################################################################################

################################################################################
class RenderDirty(RenderUpdates):
    ############################################################################
    def draw(self, rSurface):
        """
        OVERRIDE - Ensures that only the 'Dirty' parts of the screen (the bits
            that have changed since the last draw) are redrawn
        """
        lDirty = self.lostsprites
        self.lostsprites = []
        
        for rSprite, rCurrentRect in self.spritedict.items():
            # Don't bother with undirty sprites
            if not rSprite.IsDirty(): continue
            # Blit sprite to screen
            rNewRect = rSurface.blit(rSprite.image, rSprite.rect)
            if rCurrentRect is 0:
                # If no current rect, just need to update new rect
                lDirty.append(rNewRect)
            else:
                if rNewRect.colliderect(rCurrentRect):
                    # If new and current rects collide, we need to update
                    # the mishmash
                    lDirty.append(rNewRect.union(rCurrentRect))
                else:
                    # Otherwise we update both the new and current rects
                    lDirty.append(rNewRect)
                    lDirty.append(rCurrentRect)
            # Update sprite dictionary with new position
            self.spritedict[rSprite] = rNewRect
        return lDirty
    ############################################################################
    
    ############################################################################
    def update(self):
        for rSprite in self.sprites():
            if not rSprite.GetClient():
                self.remove(rSprite)
        
        RenderUpdates.update(self)
        
        # Check for collisions
        for rSprite, rCurrentRect in self.spritedict.items():
            rColSprites = spritecollide(rSprite, self, False)
            # Only counts if collided with more than just self!
            if len(rColSprites) > 1: rSprite.SetDirty(1)
    ############################################################################
################################################################################